Layer (Effect)
Tag (Effect)とimplをまとめたもの
という意味ではContextなどとも同じ
Service (Effect)を定義する際に、サービスの実装に必要な依存関係を隠蔽できる
そのため、再利用やテスト時の利用がしやすくなる
Serviceの依存関係を隠蔽できる
実装用のLayer、テスト用のLayer、みたいなのを作っておけば、それを入れ替えて使える
Managing Layers | Effect Documentation
型
Layer<Out, Error, In>
命名規則
HogeLive
HogeTest
Layerを作る
Layer.succeed
Layer.effect
Layer.succeedContext
Layerの合成
LayerとLayerからLayerを作る
Layer.merge
Layer.mergeAll
Layer.provide
Layerが必要な背景 ref
code:ts
import { Effect, Context } from "effect"
class Config extends Context.Tag("Config")<Config, {}>() {}
class Logger extends Context.Tag("Logger")<Logger, {}>() {}
class Database extends Context.Tag("Database")<
Database,
{
readonly query: (
sql: string
) => Effect.Effect<unknown, never, Config | Logger> // ①
}
() {}
Databaseは、ConfigとLoggerに依存する
しかし、Tagの定義時に、その依存関係という詳細をinterfaceに明示したくない
何故か?
型で明示すると、その依存が露出するのでDatabase使用時に毎回それを解消する必要がある
例えば、Databaseのテストをするときに、わざわざConfigとLoggerも必要になる
要するに、①のRをneverにしたい
Effect.Effect<unknown, never, never>にしたい
Layerを使えばそれができる
Layer定義の例
他のService (Effect)に依存しないシンプルなLayerの例
code:ts
// ConfigLive :: Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({ logLevel: "INFO", connection: "..." })
})
Layer<Config, never, never>の意味
Layerを構築するとConfig Serviceが生成される
Layerの構築には失敗しない
Layerには依存関係がない
Config.ofを使ったほうが型が厳しくチェックされる
code:ts
const ConfigLive = Layer.succeed(
Config,
Config.of({
getConfig: Effect.succeed({ logLevel: "INFO", connection: "..." })
})
)
Logger Layer
Configに依存するLayerを定義する
code:ts
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> } // ①
() {}
// LoggerLive :: Layer<Logger, never, Config> ②
const LoggerLive = Layer.effect(Logger, Effect.gen(function* () {
const config = yield* Config
return {
log: (msg) => Effect.gen(function* () {
const { logLevel } = yield* config.getConfig
console.log([${logLevel}] ${msg})
})
}
}))
tagの定義①では、RにConfigがいない
実装②では、RにConfigがいる
Database Layer
ConfigとLoggerに依存するLayer
code:ts
const DatabaseLive = Layer.effect(Database, Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
return {
query: (sql) => Effect.gen(function* () {
yield* logger.log(Executing query: ${sql})
const { connection } = yield* config.getConfig
return { result: Results from ${connection} }
})
}
}))
#wip
Layerのメモ化
Layer.launch
Layer.tap
https://effect.website/docs/requirements-management/layers/#tapping
Layer が 正常に構築された場合に何かをする
たとえば、成功したログを出す用途などに使う
code:ts
Layer.tap((ctx) => Console.log(Succeeded with: ${ctx}))
Layer.tapError
https://effect.website/docs/requirements-management/layers/#tapping
Layer が 失敗したときに何かをする。
たとえば、エラー時のログや通知などに使います。
code:ts
Layer.tapError((err) => Console.log(Failed with: ${err}))
Layer.catchAll
Layer.orElse
🧪 テスト用のモックを使う
code:ts
const FileSystemTest = FileSystem.layerNoop({
readFileString: () => Effect.succeed("File Content...")
})
実ファイルシステムを使わず、常に "File Content..." を返すモック。
Cache.DefaultWithoutDependencies に加えて、このモックを提供すればテスト可能。
🔁 サービスそのものをモックする
code:ts
const cache = new Cache({
lookup: () => Effect.succeed("Cache Content...")
})
const runnable = program.pipe(Effect.provideService(Cache, cache))
依存関係を差し替えるのではなく、Cache 自体を差し替える例。